using System;
using System.Text;
using System.Reflection;
using System.Configuration;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Globalization;

using ca.metaobjects.csogateway.Configuration;
using ca.metaobjects.csogateway.Converters.Interfaces;
using ca.metaobjects.csogateway.Reflection;

namespace ca.metaobjects.csogateway
{
	/// <summary>
	/// <para>
	/// The ToClientSideMainConverter class is the main entry point for converting server-side objects into client-side values. More specifically, it provides
	/// the following functionnalities:
	/// <list type="bullet">
	/// <item>Recursively go through the composition graph of any cso-compatible object and generate a string of javascript code that, when evaluated,
	/// create an equivalent client-side value.</item>
	/// <item>Provides the to-client-side conversion functionalities for types corresponding to cso-types (cso-generated types and cso-native types).</item>
	/// <item>Provides the to-client-side conversion functionalities for .NET primitive types and many other commonly .NET types like System.DateTime and System.Array.</item>
	/// </list>
	/// </para>
	/// <para>
	/// Here an exaustive list of the .NET types handled by this class
	/// </para>
	/// <para>
	/// .NET primitive data types
	/// <table>
	///		<tr><th>TYPE</th>					<th>JAVASCRIPT TYPE</th>		<th>COMMENTS</th></tr>
	///		<tr><td>System.Boolean</td>			<td>Boolean</td>				<td>-</td></tr>
	///		<tr><td>System.Byte</td>			<td>Number</td>					<td>-</td></tr>
	///		<tr><td>System.SByte</td>			<td>Number</td>					<td>-</td></tr>
	///		<tr><td>System.Int16</td>			<td>Number</td>					<td>-</td></tr>
	///		<tr><td>System.UInt16</td>			<td>Number</td>					<td>-</td></tr>
	///		<tr><td>System.Int32</td>			<td>Number</td>					<td>-</td></tr>
	///		<tr><td>System.UInt32</td>			<td>Number</td>					<td>-</td></tr>
	///		<tr><td>System.Int64</td>			<td>Number</td>					<td>Range limited. See <see cref="ca.metaobjects.csogateway.Configuration.ToClientSideConfigElement.ExceptionOnPrecisionLossInt64" /></td></tr>
	///		<tr><td>System.UInt64</td>			<td>Number</td>					<td>Range limited. See <see cref="ca.metaobjects.csogateway.Configuration.ToClientSideConfigElement.ExceptionOnPrecisionLossUInt64" /></td></tr>
	/// 	<tr><td>System.Char</td>			<td>String</td>					<td>-</td></tr>
	/// 	<tr><td>System.Double</td>			<td>Number</td>					<td>-</td></tr>
	/// 	<tr><td>System.Single</td>			<td>Number</td>					<td>-</td></tr>
	/// </table>
	/// </para>
	/// <para>
	/// Common data structure in .NET
	/// <table>
	///		<tr><th>TYPE</th>					<th>JAVASCRIPT TYPE</th>		<th>COMMENTS</th></tr>
	/// 	<tr><td>System.String</td>			<td>String</td>					<td>-</td></tr>
	/// 	<tr><td>System.Array</td>			<td>Array</td>					<td>N-dimensional arrays are supported. Contructor of Array core object is used.</td></tr>
	/// 	<tr><td>System.DateTime</td>		<td>Date</td>					<td>Date is a javacript predefined core object not a data type. Underlying data type is Number.</td></tr>
	///		<tr><td>System.Decimal</td>			<td>Number</td>					<td>Range limited. See <see cref="ca.metaobjects.csogateway.Configuration.ToClientSideConfigElement.ExceptionOnPrecisionLossDecimal" /></td></tr>
	/// </table>
	/// </para>
	/// <para>
	/// Special cases
	/// <table>
	///		<tr><th>TYPE</th>									<th>JAVASCRIPT TYPE</th>						<th>COMMENTS</th></tr>
	///		<tr><td>#any null reference#</td>					<td>Null</td>									<td>Null is a javascript 'Special Type'</td></tr>
	/// </table>
	/// </para>
	/// </summary>
    public class ToClientSideMainConverter : IToClientSideConverter	
    {
		/// <summary>
		/// Offset between System.DateTime and the javascript Date predefined core object. DateTime for January 1st 1970 UTC time.
		/// </summary>
		public readonly static DateTime JAN_1_1970_UTC = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);

		/// <summary>
		/// Implementation of <see cref="IToClientSideConverter.RenderObjectInstantiation"/>. Calls <see cref="ToClientSideMainConverter.RenderCsoCompatibleObject"/>
		/// </summary>
		/// <param name="sourceObj">A cso-compatible object</param>
		/// <param name="builder">The StringBuilder to write the generated javascript code to.</param>
		public void RenderObjectInstantiation(object sourceObj, StringBuilder builder)
		{
			RenderCsoCompatibleObject(sourceObj, builder);
		}

		/// <summary>
		/// Recursively goes through the composition graph of the given cso-compatible object and generates a string of javascript code that, when evaluated, creates a
		/// value equivalent to the given object.
		/// The generated javascript code is written to the given StringBuilder object.
		/// </summary>
		/// <param name="csoCompatibleObject">A cso-compatible object</param>
		/// <param name="builder">The StringBuilder to write the generated javascript code to.</param>
        public static void RenderCsoCompatibleObject(object csoCompatibleObject, StringBuilder builder)
		{
			RenderObject(csoCompatibleObject, builder);
		}

		private static void RenderString(String s, StringBuilder builder)
		{
			builder.Append("'");
			for (int i = 0; i < s.Length; ++i)
			{
				if (s[i] == '\\')
					builder.Append(@"\\");
				else if (s[i] == '\'')
					builder.Append(@"\'");
				else if (s[i] == '"')
					builder.Append("\\\"");
				else
					builder.Append(s[i]);
			}
			builder.Append("'");
		}

		private static void RenderBoolean(Boolean b, StringBuilder builder)
		{
			if (b)
				builder.Append("true");
			else
				builder.Append("false");
		}

		private static void RenderInteger(Object obj, StringBuilder builder)
		{
			if (obj is UInt32)
			{
				builder.Append(Convert.ToUInt32(obj).ToString("D", CultureInfo.InvariantCulture));
			}
			else
			{
				builder.Append(Convert.ToInt32(obj).ToString("D", CultureInfo.InvariantCulture));
			}
		}

		private static void RenderLongInteger(Object obj, StringBuilder builder)
		{
			if (obj is System.Int64)
			{
				long longInteger = (long)obj;
				if (ConfigurationProxy.Instance.CsoGatewayConfig.ToClientSideConfig.ExceptionOnPrecisionLossInt64)
				{
					if ((longInteger < -9007199254740992L) || (longInteger > 9007199254740992L))
						throw new CsoGatewayException(ExceptionMessage.OG_000, longInteger.ToString("D", CultureInfo.InvariantCulture));
				}
				builder.Append(((long)longInteger).ToString("D", CultureInfo.InvariantCulture));
			}
			else // obj is System.UInt64
			{
				ulong longInteger = (ulong)obj;
				if (ConfigurationProxy.Instance.CsoGatewayConfig.ToClientSideConfig.ExceptionOnPrecisionLossUInt64)
				{
					if ((longInteger > 9007199254740992UL))
						throw new CsoGatewayException(ExceptionMessage.OG_001, longInteger.ToString("D", CultureInfo.InvariantCulture));
				}	
				builder.Append(((ulong)longInteger).ToString("D", CultureInfo.InvariantCulture));
			}
		}
		
		private static void RenderFloat(Object obj, StringBuilder builder)
		{
			float floatValue = (float)obj;

			if (Single.IsNaN(floatValue))
				builder.Append("Number.NaN");
			else if (floatValue == Single.NegativeInfinity)
				builder.Append("Number.NEGATIVE_INFINITY");
			else if (floatValue == Single.PositiveInfinity)
				builder.Append("Number.POSITIVE_INFINITY");
			else
				builder.Append(floatValue.ToString("R", CultureInfo.InvariantCulture));
		}

		private static void RenderDouble(Object obj, StringBuilder builder)
		{
			double doubleValue = (double)obj;

			if (Double.IsNaN(doubleValue))
				builder.Append("Number.NaN");
			else if (doubleValue == Double.NegativeInfinity)
				builder.Append("Number.NEGATIVE_INFINITY");
			else if (doubleValue == Double.PositiveInfinity)
				builder.Append("Number.POSITIVE_INFINITY");
			else
				builder.Append(doubleValue.ToString("R", CultureInfo.InvariantCulture));
		}
		
		private static void RenderDecimal(Object obj, StringBuilder builder)
		{
			Decimal decimalValue = (Decimal)obj;
			Double doubleValue = Convert.ToDouble(decimalValue);

			if (ConfigurationProxy.Instance.CsoGatewayConfig.ToClientSideConfig.ExceptionOnPrecisionLossDecimal)
			{
				decimal roundTripDecimalValue;
				try
				{
					roundTripDecimalValue = Convert.ToDecimal(doubleValue);
				}
				catch (OverflowException)
				{
					throw new CsoGatewayException(ExceptionMessage.OG_002, decimalValue.ToString("N", CultureInfo.InvariantCulture));
				}

				if (roundTripDecimalValue != decimalValue)
				{
					throw new CsoGatewayException(ExceptionMessage.OG_002, decimalValue.ToString("N", CultureInfo.InvariantCulture));
				}
			}
			builder.Append(Convert.ToDouble(decimalValue).ToString("R", CultureInfo.InvariantCulture));
		}
		
		private static void RenderDate(DateTime date, StringBuilder builder)
		{
			TimeSpan span = (date.ToUniversalTime() - JAN_1_1970_UTC);
			
			builder.Append("new Date(");
			builder.Append(Math.Floor(span.TotalMilliseconds).ToString("R",CultureInfo.InvariantCulture));
			builder.Append(")");
		}

		private static void RenderArray(System.Array array, StringBuilder builder, int iterationRank, int[] indexes)
		{
			builder.Append("new Array(");

			int[] newIndexes = new int[indexes.Length];
			Array.Copy(indexes, newIndexes, indexes.Length);
			for (int i = 0; i <= array.GetUpperBound(iterationRank); ++i)
			{
				newIndexes[iterationRank] = i;
					
				if (iterationRank == (array.Rank - 1))
				{
					RenderObject(array.GetValue(newIndexes), builder);
				}
				else
				{
					RenderArray(array, builder, iterationRank + 1, newIndexes);
				}

				if (i < (array.GetUpperBound(iterationRank)))
				{
					builder.Append(',');
				}
			}

			builder.Append(")");
		}

		/// <summary>
		/// Renders the specified System.Guid as 32 digits separated by hyphens:
		/// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
		/// and append the result to the System.StringBuilder provided.
		/// </summary>
		/// <param name="guid">The Guid to render.</param>
		/// <param name="builder">The StringBuilder the Guid will be rendered in.</param>
		private static void RenderGuid(Guid guid, StringBuilder builder)
		{
			builder.Append(guid.ToString("D", CultureInfo.InvariantCulture));
		}

		private static void RenderEquivalentCsoGeneratedType(object obj, StringBuilder builder)
        {
			CsoGeneratedType csoGenType = (CsoGeneratedType)Loader.getCsoTypeForType(obj.GetType());
			
			DependencyGraph.ForCurrentHttpRequest.fillDependencyGraphForCsoType(csoGenType);
			
			if (obj is System.Enum)
			{
				if (Enum.IsDefined(obj.GetType(), obj))
				{
					builder.Append(csoGenType.FullTypeName);
					builder.Append(".");
					builder.Append(Enum.GetName(obj.GetType(), obj));
				}
				else
				{
					builder.Append(((System.Enum)obj).ToString("D"));
				}
			}
			else
			{
				builder.Append("new ");
				builder.Append(csoGenType.FullTypeName);
				builder.Append("(");

				List<PropertyInfo> properties = csoGenType.DefinedProperties;
				object[] constructorParamValueList = new object[properties.Count];

				for (int i = 0; i < properties.Count; i++)
				{
					constructorParamValueList[i] = (properties[i].GetGetMethod(false).Invoke(obj, null));
				}

				RenderObjectArray(constructorParamValueList, builder, ",");

				builder.Append(")");
			}
        }

		private static void RenderEquivalentCsoNativeType(object obj, StringBuilder builder)
		{
			CsoNativeType csoNatType = (CsoNativeType)Loader.getCsoTypeForType(obj.GetType());
			DependencyGraph.ForCurrentHttpRequest.fillDependencyGraphForCsoType(csoNatType);

			Type converterType = Loader.getConverterTypeForType(obj.GetType());
			IToClientSideConverter converter = (IToClientSideConverter) converterType.GetConstructor(new Type[0]).Invoke(new object[0]);
			converter.RenderObjectInstantiation(obj, builder);
			
		}

		private static void RenderObjectArray(object[] objectArray, StringBuilder builder, String separator)
		{
			for (int i = 0; i < objectArray.Length; ++i)
			{
				RenderObject(objectArray[i], builder);

				if (i < (objectArray.Length - 1))
				{
					builder.Append(separator);
				}
			}
		}

		private static void RenderObject(object obj, StringBuilder builder)
		{
			bool isNull = false;
			bool isString = false;
			bool isBoolean = false;
			bool isInteger = false;
			bool isLongInteger = false;
			bool isFloat = false;
			bool isDouble = false;
			bool isDecimal = false;
			bool isDateTime = false;
			bool isArray = false;
			bool isGuid = false;

			bool hasCsoGeneratedTypeEquivalent = false;
			bool hasCsoNativeTypeEquivalent = false;
			bool isCsoReady = false;
			
			isNull = (obj == null);

			if (!isNull)
			{
				isString = (obj is System.String) || (obj is System.Char);
				isBoolean = obj is System.Boolean;
				isInteger = (
					obj is System.SByte ||
					obj is System.Byte ||
					obj is System.Int16 ||
					obj is System.UInt16 ||
					obj is System.Int32 ||
					obj is System.UInt32);
				isLongInteger = (
					obj is Int64 ||
					obj is UInt64);
				isFloat = obj is System.Single;
				isDouble = obj is System.Double;
				isDecimal = obj is Decimal;
				isDateTime = obj is DateTime;
				isArray = obj is Array;
				isGuid = obj is Guid;

				// Even if obj is Nullable,  obj.GetType() returns the underlying type
				hasCsoGeneratedTypeEquivalent = Loader.csoGeneratedTypeExistFor(obj.GetType());
				hasCsoNativeTypeEquivalent = Loader.csoNativeTypeExistFor(obj.GetType());
				isCsoReady = Loader.isCsoReady(obj.GetType());
			}


			if (isNull)
			{
				builder.Append("null");
			}
			else if (isString)
			{
				RenderString(obj.ToString(), builder);
			}
			else if (isBoolean)
			{
				RenderBoolean((Boolean)obj, builder);
			}
			else if (isInteger)
			{
				RenderInteger(obj, builder);
			}
			else if (isLongInteger)
			{
				RenderLongInteger(obj, builder);
			}
			else if (isFloat)
			{
				RenderFloat(obj, builder);
			}
			else if (isDouble)
			{
				RenderDouble(obj, builder);
			}
			else if (isDecimal)
			{
				RenderDecimal(obj, builder);
			}
			else if (isDateTime)
			{
				RenderDate((DateTime)obj, builder);
			}
			else if (isArray)
			{
				Array arrayObject = (Array) obj;
				int[] indexes = new int[arrayObject.Rank];
				
				RenderArray(arrayObject, builder, 0, indexes);
			}
			else if (isGuid)
			{
				RenderGuid((Guid)obj, builder);
			}
			else if (hasCsoGeneratedTypeEquivalent)
			{
				RenderEquivalentCsoGeneratedType(obj, builder);
			}
			else if (hasCsoNativeTypeEquivalent)
			{
				RenderEquivalentCsoNativeType(obj, builder);
			}
			else if (isCsoReady)
			{
				Loader.createCsoGeneratedType(obj.GetType());
				RenderObject(obj, builder);
			}
			else
			{
				throw new CsoGatewayException(ExceptionMessage.OG_003, obj.ToString(), obj.GetType().FullName);
			}
		}
    }
}
